Skip to content

📥 下载字模工具: 点击下载 PC2LCD2021.exe

以下为懒人专属!

📥 下载font.c: 点击下载 font.c

📥 下载font.h: 点击下载 font.h

同学!你已经成功点亮了屏幕并且能显示纯色,还用了DMA加速,这说明你的基础驱动已经非常扎实了。现在我们要让屏幕“说话”——显示字符和文字。这其实就像在屏幕上“画”出字母的形状,而字模工具就是帮我们把字母形状翻译成“点阵地图”的翻译官。

下面我会用最通俗的方式,一步步带你写出显示字符的代码,并且把原理讲透,让你不仅会抄代码,更明白为什么这么写。

一、先理解字模是什么(像素小灯泡的开关图)

字模工具界面 字模工具界面(.图)

想象一下,你的屏幕是由很多个微小的彩色小灯泡(像素)组成的。要显示一个字母“A”,我们需要决定哪些小灯泡亮(前景色),哪些不亮(背景色)。
字模文件就是一个“开关图”:它用二进制位(0或1)记录了每个位置的小灯泡是亮(1)还是灭(0)。
比如一个宽8像素、高16像素的字符,就是8列 × 16行 = 128个小灯泡,每个灯泡用1个bit表示,总共128bit = 16个字节。

你下载的 **PCtoLCD2021 工具生成的就是这种数组。你给的例子:

c
unsigned char fontArray[] = 
{
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,    // 空格
    0x00,0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00,0x10,0x10,0x00,0x00,    // '!'
    ...
};

问题来了:这16个字节是如何对应到8×16个点的呢?
通常,字模工具采用 “列行式” 排列:

  • 第一个字节表示 第0列 的从上到下的8个点(因为高度16,所以分上下两半,每列用2个字节,先上半8位,再下半8位?但这里宽度8,高度16,每个字符16字节,所以很可能是每列用2个字节(共8列),每列从上到下16个点。
  • 但是观察'!'的第二个字节:0x00,0x00,0x00,0x10,0x10,...,如果是每列2字节,那么第0列是0x00,0x00(全灭),第1列是0x00,0x10? 这不太像。更常见的格式是 “每列一个字节,但高度为8”,而高度16则用两行(上下各8像素)表示。
    我们来看'!'的数据:
0x00,0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00,0x10,0x10,0x00,0x00

如果按 每列一个字节(8位),需要16列才能表示16行,但宽度是8,所以不对。
如果按 每行一个字节(8位),需要16行,每行8位,正好16个字节。那么每个字节表示一行中8个像素的亮灭(从左到右)。
看'!'的数据:

  • 第0行:0x00 -> 全灭
  • 第1行:0x00 -> 全灭
  • 第2行:0x00 -> 全灭
  • 第3行:0x10 -> 二进制 00010000,也就是第4个像素亮(从0开始数第4位)
  • 第4行:0x10 -> 同上
  • ...一直到第9行都是0x10,说明中间有一条竖线。
  • 第10行:0x00
  • 第11行:0x00
  • 第12行:0x10
  • 第13行:0x10
  • 第14行:0x00
  • 第15行:0x00

所以很明显:每个字节表示一行(从上到下共16行),每个字节的8个位表示从左到右的8个像素。高位(bit7)对应最左边的像素,低位(bit0)对应最右边的像素。
这就是“横向取模”方式,非常直观。

二、如何显示一个字符(我们的作战计划)

2.1 认识ASCII码表

ASCII表:32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字

Bin

(二进制)
Oct

(八进制)
Dec

(十进制)
Hex

(十六进制)
缩写/字符解释
0000 00000000x00NUL(null)空字符
0000 00010110x01SOH(start of headline)标题开始
0000 00100220x02STX (start of text)正文开始
0000 00110330x03ETX (end of text)正文结束
0000 01000440x04EOT (end of transmission)传输结束
0000 01010550x05ENQ (enquiry)请求
0000 01100660x06ACK (acknowledge)收到通知
0000 01110770x07BEL (bell)响铃
0000 100001080x08BS (backspace)退格
0000 100101190x09HT (horizontal tab)水平制表符
0000 1010012100x0ALF (NL line feed, new line)换行键
0000 1011013110x0BVT (vertical tab)垂直制表符
0000 1100014120x0CFF (NP form feed, new page)换页键
0000 1101015130x0DCR (carriage return)回车键
0000 1110016140x0ESO (shift out)不用切换
0000 1111017150x0FSI (shift in)启用切换
0001 0000020160x10DLE (data link escape)数据链路转义
0001 0001021170x11DC1 (device control 1)设备控制1
0001 0010022180x12DC2 (device control 2)设备控制2
0001 0011023190x13DC3 (device control 3)设备控制3
0001 0100024200x14DC4 (device control 4)设备控制4
0001 0101025210x15NAK (negative acknowledge)拒绝接收
0001 0110026220x16SYN (synchronous idle)同步空闲
0001 0111027230x17ETB (end of trans. block)结束传输块
0001 1000030240x18CAN (cancel)取消
0001 1001031250x19EM (end of medium)媒介结束
0001 1010032260x1ASUB (substitute)代替
0001 1011033270x1BESC (escape)换码(溢出)
0001 1100034280x1CFS (file separator)文件分隔符
0001 1101035290x1DGS (group separator)分组符
0001 1110036300x1ERS (record separator)记录分隔符
0001 1111037310x1FUS (unit separator)单元分隔符
==0010 0000====040====32====0x20====(space)====空格==
0010 0001041330x21!叹号
0010 0010042340x22"双引号
0010 0011043350x23#井号
............................................
0111 110101751250x7D}闭花括号
==0111 1110====0176====126====0x7E====~====波浪号==
0111 111101771270x7FDEL (delete)删除

ASCII码表(.表格)

2.2 我们要在屏幕上指定位置(x, y)画一个字符,步骤是:

  1. 计算字符在字模数组中的起始位置
    你的 idxArray 是 " !"#$%&'...,空格是第一个字符(序号0),'!'是第二个(序号1)。所以字符 ch 的序号 = ch - ' '(因为空格ASCII码32,'!'是33,相减得1)。
    每个字符占用16个字节,所以 offset = (ch - ' ') * 16,然后从 fontArray[offset] 开始取16个字节。

  2. 遍历这16个字节(即16行)
    对于每一行 row(0~15),取出该行的字节 byte = fontArray[offset + row]

  3. 遍历该字节的8个位(即8个像素)
    对于列 col(0~7),检查 byte 的第 col 位(注意:最左边是最高位,即 bit7)。可以用 byte & (0x80 >> col) 判断该位是否为1。

  4. 如果该位为1,就在屏幕位置 (x + col, y + row) 画前景色;否则画背景色(或者不画,保留背景)
    你可以选择画背景色(如黑色)覆盖,也可以只画前景色(背景色事先清空)。初学者建议用“画点”函数,逐步来。

  5. 循环结束后,这个字符就显示出来了。

三、编写代码(手把手)

第一步:把字模数据放到你的工程里

你可以把生成的数组复制到一个 .h 文件,比如 font.h,然后在 main.c#include "font.h"
注意:你生成的数组可能叫 fontArrayidxArray,确保它们的名称一致。

第二步:写一个绘制单个字符的函数

main.c 中添加如下函数:

c
/**
 * @brief 在指定位置绘制一个字符(8x16像素)
 * @param x     左上角x坐标(列)
 * @param y     左上角y坐标(行)
 * @param ch    要显示的字符,如 'A'
 * @param fg    前景色(RGB565)
 * @param bg    背景色(RGB565),如果你只想画前景,可以设为透明,但简单起见我们画背景
 * @param spi    SPI设备句柄
 */
void lcd_draw_char(uint16_t x, uint16_t y, char ch, uint16_t fg, uint16_t bg, spi_device_handle_t spi)
{
    // 1. 检查字符是否在可打印范围内(32~126)
    if (ch < ' ' || ch > '~') return;

    // 2. 计算字符在字模数组中的偏移(每个字符16字节)
    int offset = (ch - ' ') * 16;

    // 3. 遍历16行
    for (int row = 0; row < 16; row++) {
        uint8_t byte = fontArray[offset + row];  // 取出这一行的位图

        // 4. 遍历8列(从左到右)
        for (int col = 0; col < 8; col++) {
            // 判断该位是否为1:从最高位(bit7)开始,向右移位
            if (byte & (0x80 >> col)) {
                // 点亮:画前景色
                lcd_draw_pixel(x + col, y + row, fg, spi);
            } else {
                // 熄灭:画背景色(如果你想保留背景,可以不画,但为了清晰我们画背景)
                lcd_draw_pixel(x + col, y + row, bg, spi);
            }
        }
    }
}

解释

  • 0x80 >> col 可以依次得到 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01,分别对应第0~7位(高位到低位)。
  • 因为我们的字模是“横向取模”,每个字节的高位对应左边像素,所以这样用。

第三步:写一个显示字符串的函数

c
/**
 * @brief 显示一个字符串,自动换行(简单版,不换行)
 * @param x     起始x
 * @param y     起始y
 * @param str   字符串
 * @param fg    前景色
 * @param bg    背景色
 * @param spi   SPI句柄
 */
void lcd_draw_string(uint16_t x, uint16_t y, const char *str, uint16_t fg, uint16_t bg, spi_device_handle_t spi)
{
    while (*str) {
        // 如果字符超出屏幕宽度(假设240宽度,每个字符8像素,我们可以选择折行,但简单起见只画能显示的)
        if (x + 8 > 240) {
            // 换行:x回到0,y增加16(字符高度)
            x = 0;
            y += 16;
            // 如果y超出屏幕高度则停止
            if (y + 16 > 320) break;
        }
        lcd_draw_char(x, y, *str, fg, bg, spi);
        x += 8;            // 字符宽度8像素
        // 可加一个像素的间隔,如 x += 9;
        str++;
    }
}

第四步:在 app_main 中调用

在你的 app_main 中,初始化屏幕后,调用显示字符串:

c
void app_main(void)
{
    // ... 你的原有初始化代码(GPIO、SPI、st7789_init)...

    // 清屏(可选),用黑色背景
    lcd_fill_color_dma(0x0000, spi_device_handle);  // 黑色

    // 显示一句英文
    lcd_draw_string(10, 20, "Hello, ESP32!", 0xFFFF, 0x0000, spi_device_handle);
    lcd_draw_string(10, 50, "ABC 123", 0x07E0, 0x0000, spi_device_handle); // 绿色前景

    // 保持显示
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

四、可能遇到的问题及调试

  1. 字符显示倒置或镜像
    如果显示出来是反的(左右镜像),你可以调整位判断方向,比如用 byte & (1 << col) 而不是 0x80 >> col;如果是上下颠倒,那就调整 row 的遍历顺序。但按照你的字模(每个字节高位对应左边),上述代码应该是正确的。

  2. 背景色覆盖
    我用了 bg 颜色画每个像素,这会导致字符显示在纯色背景上。如果你希望背景透明(即只画前景,背景不变),那就把 else 部分去掉,只画前景。但要注意屏幕原本可能有杂色,所以最好先清屏。

  3. 性能问题
    lcd_draw_pixel 每画一个像素都要设置窗口并发送SPI数据,非常慢(显示一个字符需要 8×16=128 次SPI传输)。这只是教学用,让你理解原理。后续你可以优化:

    • 用DMA一次性发送一行的8个像素(甚至整行),类似你图片显示的方式。
    • 或者先构建一个显存缓冲区,然后一次性DMA发送。

五、进阶优化思路(等你熟练后再看)

  • 使用窗口+批量发送:比如画一个字符时,设置窗口为 (x, y) 到 (x+7, y+15),然后像图片那样一次性发送 16×8×2 = 256 字节(每个像素2字节颜色)。需要把字模的位转换成颜色数据(前景色或背景色)。这比挨个画点快得多。
  • 使用DMA:你可以分配一个缓冲区,填充好颜色数据,然后用 spi_device_transmit 一次发送,就和显示图片一样。

但作为小白,先掌握逐个画点的方法,理解原理后再优化也不迟。


六、核心要点总结(形象比喻)

  • 字模 = 一张“亮灭地图”,用二进制0/1记录每个小灯泡要不要亮。
  • 显示字符 = 拿着这张地图,走到屏幕的某个位置,按照地图上每个点的指示,点亮或熄灭对应位置的灯泡。
  • 偏移计算 = 找到你要的字符的地图在仓库(数组)里的位置,因为每个地图大小固定(16字节),所以用序号×16即可。
  • 位操作 = 检查地图上某一点是1还是0,我们使用 & 和移位来取出特定位。

现在,尝试把你的字模数组添加进去,编译烧写,看看屏幕上有没有出现“Hello, ESP32!”吧!如果出现乱码,检查字模格式(方向、位序)是否正确,调整一下位判断的方向即可。

加油!你已经从“点亮屏幕”进阶到“让屏幕说话”了!🎉

觉醒,然后燎原。 © 2026 门主引擎